﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Xml.Serialization;
using Sharebert.DownloadLibrary.Interfaces;
using System.Net;
using System.Diagnostics;

namespace Sharebert.DownloadLibrary
{
    /// <summary>
    /// A DownloadClient enables the user to download a file from a DownloadListener
    /// By using RequestFile(filename,filesize) the client asks the DownloadListener if it
    /// has a file fitting to name/size.
    /// </summary>
    public class DownloadClient : IDownloadClient
    {
        private readonly IPAddress _remoteIp;
        private readonly int _remotePort;
        private TcpClient _client;
        private bool _running;
        private int _timeout = 30000;

        /// <summary>
        /// A DownloadClient which can download a file from a DownloadListener listening
        /// on the remoteIp and the remotePort
        /// </summary>
        /// <param name="remoteIp"></param>
        /// <param name="remotePort"></param>
        public DownloadClient(IPAddress remoteIp, int remotePort)
        {
            _remoteIp = remoteIp;
            _remotePort = remotePort;
        }

        private struct Filestruct
        {
            public string Filename;
            public string Tempfilename;
            public int Filesize;
        }

        /// <summary>
        /// Ask the DownloadListener on the "other side" if it has such a file
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="filesize"></param>
        public void RequestFile(string filename, string tempfilename, int filesize)
        {
            if (_running)
            {
                throw new InvalidOperationException("Request already started!");
            }
            _running = true;
            var endpoint = new IPEndPoint(_remoteIp, _remotePort);
            _client = new TcpClient();
            _client.ReceiveTimeout = Timeout;
            _client.SendTimeout = Timeout;
            _client.Connect(endpoint);
            var clientThread = new Thread(new ParameterizedThreadStart(HandleRequestFile));
            clientThread.Name = "ClientThread";
            var filestruct = new Filestruct() {Filename = filename, Filesize = filesize, Tempfilename = tempfilename};
            clientThread.Start(filestruct);
        }

        /// <summary>
        /// handle the download (if a download is possible)
        /// </summary>
        /// <param name="fs"></param>
        private void HandleRequestFile(object fs)
        {
            try
            {
                var filestruct = (Filestruct) fs;
                var networkStream = _client.GetStream();
                var message = new Message();
                message.Header.MessageType = MessageType.FileRequestMessage;
                var requestMessage = new RequestMessage();
                requestMessage.Filename = filestruct.Filename;
                requestMessage.Filesize = filestruct.Filesize;
                var serializer = new XmlSerializer(typeof (RequestMessage));
                var stream = new MemoryStream();
                serializer.Serialize(stream, requestMessage);
                message.Payload = stream.ToArray();
                try{
                    Message.SendMessage(networkStream, message);
                }
                catch (IOException ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine(ex.StackTrace);
                    if (TimedOut != null)
                    {
                        var args = new EventArgs();
                        TimedOut.Invoke(this, args);
                        return;
                    }
                }

                FileStream fileStream = null;
                int totalbytes = 0;
                int flushcount = 0;
                Stopwatch watch = new Stopwatch();
                while (_client.Connected)
                {
                    watch.Reset();
                    watch.Start();
                    try
                    {
                        message = Message.ReceiveMessage(networkStream);
                    }
                    catch(IOException ex)
                    {
                        Console.WriteLine(ex.Message);
                        Console.WriteLine(ex.StackTrace);
                        if(TimedOut != null)
                        {
                            var args = new EventArgs();
                            TimedOut.Invoke(this,args);
                            return;
                        }
                    }
                    watch.Stop();
                    var messageDownloadDuration = watch.Elapsed;

                    switch (message.Header.MessageType)
                    {
                        case MessageType.FileNotFoundMessage:
                            if (FileNotFound != null)
                            {
                                serializer = new XmlSerializer(typeof (FileNotFoundMessage));
                                var memoryStream = new MemoryStream(message.Payload);
                                var fileNotFoundMessage = (FileNotFoundMessage) serializer.Deserialize(memoryStream);
                                var args = new FileNotFoundEventArgs();
                                args.Filename = fileNotFoundMessage.Filename;
                                args.Filesize = fileNotFoundMessage.Filesize;
                                FileNotFound.Invoke(this, args);
                                _client.Close();
                            }
                            break;

                        case MessageType.FilePartMessage:
                            if (fileStream == null)
                            {
                                serializer = new XmlSerializer(typeof (FilePartMessage));
                                var memoryStream = new MemoryStream(message.Payload);
                                var filePartMessage = (FilePartMessage) serializer.Deserialize(memoryStream);
                                fileStream = File.OpenWrite(filestruct.Tempfilename);
                                fileStream.SetLength(filePartMessage.Filesize);
                                fileStream.Write(filePartMessage.Bytes, 0, filePartMessage.Bytes.Length);
                                
                                if (DownloadStatusChanged != null)
                                {
                                    var bytespersecond = 0;
                                    try
                                    {
                                        bytespersecond = ((message.Header.Size + 10)/
                                                          (1 + (int) messageDownloadDuration.TotalMilliseconds)*1000);
                                    }
                                    catch(Exception)
                                    {
                                        //wtf?
                                    }
                                    totalbytes = totalbytes + filePartMessage.Bytes.Length;
                                    var args = new DownloadStatusEventArgs();
                                    args.BytesPerSecond = bytespersecond;
                                    args.Filename = filePartMessage.Filename;
                                    args.Filesize = filePartMessage.Filesize;
                                    args.PercentageFinished = (double) totalbytes/(double) filePartMessage.Filesize;
                                    DownloadStatusChanged.Invoke(this, args);

                                }
                                if (filePartMessage.LastOne)
                                {
                                    _client.Close();
                                    fileStream.Close();
                                    if (FileCompleted != null)
                                    {
                                        var args = new FileCompletedEventArgs();
                                        args.Filename = filePartMessage.Filename;
                                        args.Filesize = filePartMessage.Filesize;
                                        args.Tempfilename = filestruct.Tempfilename;
                                        FileCompleted.Invoke(this, args);
                                    }                                    
                                }
                            }
                            else
                            {
                                serializer = new XmlSerializer(typeof (FilePartMessage));
                                var memoryStream = new MemoryStream(message.Payload);
                                var filePartMessage = (FilePartMessage) serializer.Deserialize(memoryStream);                                
                                fileStream.Seek(filePartMessage.Offset, SeekOrigin.Begin);
                                fileStream.Write(filePartMessage.Bytes, 0, filePartMessage.Bytes.Length);
                                flushcount++;
                                if (flushcount == 100)
                                {
                                    fileStream.Flush();
                                    flushcount = 0;
                                }
                                if (DownloadStatusChanged != null)
                                {
                                    var bytespersecond = 0;
                                    try
                                    {
                                        bytespersecond = ((message.Header.Size + 10) /
                                                          (1 + (int)messageDownloadDuration.TotalMilliseconds) * 1000);
                                    }
                                    catch (Exception)
                                    {
                                        //wtf?
                                    }
                                    totalbytes = totalbytes + filePartMessage.Bytes.Length;
                                    var args = new DownloadStatusEventArgs();
                                    args.BytesPerSecond = bytespersecond;
                                    args.Filename = filePartMessage.Filename;
                                    args.Filesize = filePartMessage.Filesize;
                                    args.PercentageFinished = (double) totalbytes/(double) filePartMessage.Filesize;
                                    DownloadStatusChanged.Invoke(this, args);

                                }
                                if (filePartMessage.LastOne)
                                {
                                    _client.Close();
                                    fileStream.Flush();
                                    fileStream.Close();
                                    if (FileCompleted != null)
                                    {
                                        var args = new FileCompletedEventArgs();
                                        args.Filename = filePartMessage.Filename;
                                        args.Filesize = filePartMessage.Filesize;
                                        args.Tempfilename = filestruct.Tempfilename;
                                        FileCompleted.Invoke(this, args);
                                    }                                    
                                }
                            }
                            break;
                    }
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                if(ErrorOccurred != null)
                {
                    var args = new ErrorArgs();
                    args.Exception = ex;
                    ErrorOccurred.Invoke(this,args);
                }
                return;
            }
            finally
            {
                _running = false;
            }
        }

        /// <summary>
        /// Stop actual download
        /// </summary>
        public void StopDownload()
        {
            try
            {
                _client.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                if (ErrorOccurred != null)
                {
                    var args = new ErrorArgs();
                    args.Exception = ex;
                    ErrorOccurred.Invoke(this, args);
                }
                return;
            }
        }

        /// <summary>
        /// The read/write timeout for the network communication
        /// </summary>
        public int Timeout
        {
            get { return _timeout; }
            set { _timeout = value; }
        }

        /// <summary>
        /// Fires if the DownloadListener tells us that is does not has the file
        /// </summary>
        public event EventHandler<FileNotFoundEventArgs> FileNotFound;

        /// <summary>
        /// Fired if the download of the file is completed
        /// </summary>
        public event EventHandler<FileCompletedEventArgs> FileCompleted;

        /// <summary>
        /// Fired each time a new part of the file is downloaded
        /// </summary>
        public event EventHandler<DownloadStatusEventArgs> DownloadStatusChanged;

        /// <summary>
        /// Fired if the communication to the DownloadListener timed out
        /// </summary>
        public event EventHandler<EventArgs> TimedOut;

        /// <summary>
        /// In case of an error this event is fired
        /// </summary>
        public event EventHandler<ErrorArgs> ErrorOccurred;
    }
}
